1-3. 메모리 경계 계열

관통 질문: 이 읽기/쓰기가 버퍼의 경계(시작·끝·인덱스 범위) 안에 있다고 누가 보장하는가? C는 배열이나 포인터로 버퍼 밖을 읽고 써도 언어가 막아주지 않는다. 오류가 나는 게 아니라 “옆에 있는 메모리”가 조용히 바뀐다. 6개 항목은 위(끝을 넘는다)·아래(시작보다 앞선다)·옆(널 종료 없이 계속 간다)으로 방향만 다를 뿐 원인은 하나다 — “이 복사·접근이 버퍼 크기를 넘지 않는지 아무도 확인하지 않았다.”


11. 스택에 할당된 버퍼 오버플로우 CWE-121

신뢰 경계: 함수 인자 문자열 → 경계 검사 없이 → 스택 지역 버퍼로 복사 ⚠️ strcpy/strcat/sprintf/gets는 보이면 즉시 교체 대상. strncpy를 써도 마지막 바이트 널 종료는 직접 채워야 한다.

공격 시나리오: 24바이트 지역버퍼에 더 긴 문자열을 strcpy() 로 복사하면 반환주소까지 덮여, 공격자가 실행 흐름을 자신의 코드로 돌릴 수 있다.

❌ Bad

void manipulate_string(char* string)
{
    char buf[24];
    strcpy(buf, string);          /* 길이 검사 없는 복사 */
}

✅ Good

void manipulate_string(char* string)
{
    char buf[24];
    if (strlen(string) < sizeof(buf))
        strncpy(buf, string, sizeof(buf)-1);
    buf[sizeof(buf)-1] = '\0';    /* 반드시 null 로 종료 */
}

치환 규칙: strcpy → 길이 비교 후 strncpy + 마지막 바이트 수동 널 종료.


12. 힙에 할당된 버퍼 오버플로우 CWE-122

신뢰 경계: 외부 토큰 개수 → 경계 검사 없이 → malloc() 버퍼 인덱스 ⚠️ 힙이든 스택이든 “할당 크기”와 “쓰기 인덱스”는 한 쌍으로 관리하고, 복사 함수는 크기 인자를 받는 것으로 통일한다.

공격 시나리오: 토큰 개수가 할당 크기를 넘으면 힙 관리 구조체가 덮여, free() 시점에 임의 주소 쓰기로 이어져 코드 실행이나 프로세스 다운을 유발한다.

❌ Bad

char *dest = (char *)malloc(BUFSIZE);
strcpy(dest, argv[1]);                     /* 길이 검사 없음 */
while (token != NULL) {                    /* 인덱스 상한 검사 없음 */
    dataBuffer[dataIndex] = atoi(token);
    token = strtok(NULL, " "); dataIndex++; }

✅ Good

char *dest = (char *)malloc(BUFSIZE);
strlcpy(dest, argv[1], BUFSIZE);           /* 길이 제한 복사 */
while (token != NULL) {
    if (dataIndex >= MAX_DATA_BUFFER_LENGTH) break;
    dataBuffer[dataIndex] = atoi(token);
    token = strtok(NULL, " "); dataIndex++; }

치환 규칙: 힙 버퍼 복사는 strlcpy(크기 인자 필수), 반복 인덱스는 매 루프 상한 검사.


13. 버퍼 시작 지점 이전에 쓰기 CWE-124

신뢰 경계: 토큰 개수(외부 입력) → 하한 검사 없이 → 감소하는 배열 인덱스 ⚠️ 오버플로우는 위(상한)뿐 아니라 아래(하한)로도 발생한다. 감소 인덱스와 포인터 뺄셈에도 하한 검사를 붙여라.

공격 시나리오: 토큰 수가 배열 크기보다 많으면 인덱스가 0 아래로 내려가, 버퍼 앞쪽의 다른 변수나 스택 프레임이 덮인다.

❌ Bad

int wordIndex = MAX_WORD_COUNT - 1;
while (token != NULL) {            /* 하한 검사 없이 감소 */
    words[wordIndex] = token;
    token = strtok(NULL, " \t"); wordIndex--; }

✅ Good

vector<char *> words(MAX_WORD_COUNT, NULL);
try {
    while (token != NULL) {
        words.at(wordIndex) = token;    /* at() 이 범위 검사 */
        token = strtok(NULL, " \t"); wordIndex--; } }
catch (const std::exception & e) { /* out_of_range 처리 */ }

치환 규칙: 감소 루프·포인터 뺄셈에 하한(0) 검사 명시, 가능하면 범위 검사가 내장된 접근자(.at()) 사용.


14. 범위 초과해서 읽기 CWE-125

신뢰 경계: 배열 인덱스 → 상한 검사 없이 → 배열 참조 ⚠️ 배열 포인터를 넘길 때는 반드시 길이도 함께 넘긴다. “포인터 단독 전달”이 out-of-bounds read의 근본 원인이다.

공격 시나리오: 배열 뒤에 있던 세션 토큰·암호키가 반환값에 실려 그대로 흘러나가고, 값에 포인터가 섞이면 실제 주소가 드러나 ASLR이 무력화된다. 이 주소는 11·12번 버퍼 오버플로우를 정확히 겨냥하는 열쇠가 된다.

❌ Bad

int readNextData(int * data_pool) {
    static int curIdx = 0; int END_MARK = -1;
    if (data_pool[curIdx] != END_MARK)   /* 마커 없으면 폭주 */
    { curIdx++; return data_pool[curIdx]; }
    return END_MARK; }

✅ Good

int readNextData(int * data_pool, int size) {
    static int curIdx = 0; int END_MARK = -1;
    if (curIdx >= size) return END_MARK;    /* 크기 검사 먼저 */
    if (data_pool[curIdx] != END_MARK)
    { curIdx++; return data_pool[curIdx]; }
    return END_MARK; }

치환 규칙: 배열을 넘길 때 크기 인자를 항상 동반, 종료 마커만 믿지 말고 길이 기반 종료 조건을 함께 둔다.


15. 검사되지 않은 배열 인덱싱 CWE-129

신뢰 경계: 외부 정수(sscanf 결과) → 하한·상한 검사 없이 → 배열 인덱스 ⚠️ 외부에서 온 정수는 그 자체로 “인덱스 후보”일 뿐이다. 하한과 상한을 모두 검증하고 통과한 값만 [] 안에 넣어라.

공격 시나리오: 배열 접근은 base + index × 원소크기 이므로 인덱스를 조작하면 원하는 거리로 한 번에 점프해 중간 보호 장치를 건너뛴다.

❌ Bad

if (sscanf(buf, "%d %d", &num, &size) == 2)
    sizes[num-1] = size;               /* 최대·최소 검사 없음 */

✅ Good

if (sscanf(buf, "%d %d", &num, &size) == 2) {
    if (num > 0 && num <= MAXCOUNT)    /* 하한 + 상한 */
        sizes[num-1] = size;
    else { printf("에러"); return FAIL; } }

치환 규칙: 인덱스로 쓰이는 모든 외부 정수에 0 < idx <= MAX 검사, 비교 연산자는 > 대신 >= 로 경계값 실수를 줄인다.


16. 널 종료 문제 CWE-170

신뢰 경계: read()/readlink() 로 받은 버퍼 → 널 종료 보장 없이 → 문자열 함수(strcpy/strlen) ⚠️ 널 종료는 자동이 아니다. read 계열로 받은 버퍼는 “문자열”이 아니라 “바이트 배열”로 취급하라.

공격 시나리오: read() 로 읽은 버퍼는 널 종료가 보장되지 않으므로 strcpy() 가 널을 만날 때까지 계속 복사해 경계를 넘고, 인접 메모리가 유출되거나 스택이 덮인다.

❌ Bad

char buf[MAXLEN];
read(cfgfile, inputbuf, MAXLEN);        /* 널 종료 보장 없음 */
strcpy(buf, inputbuf);                  /* 널까지 계속 복사 */

✅ Good

char buf[MAXLEN];
read(cfgfile, inputbuf, MAXLEN);
strlcpy(buf, inputbuf, MAXLEN);         /* 길이 제한 + 널 종료 */
/* 또는 C++ string 객체 사용 */

치환 규칙: read/readlink 결과에 strcpy/strcat/strlen 직접 적용 금지 → strlcpy/strlcat 또는 길이 인자 명시.


이 계열 미니 체크리스트

  • strcpy · strcat · sprintf · gets 가 신규/변경 코드에 남아 있는가
  • strncpy 사용 후 마지막 바이트 널 종료를 수동으로 채웠는가
  • 감소하는 인덱스·포인터 뺄셈에 하한(0) 검사가 있는가
  • 배열을 함수에 넘길 때 크기(길이) 인자를 함께 넘기는가
  • 외부 정수를 배열 인덱스로 쓰기 전 상·하한을 모두 검사했는가
  • read()/readlink() 결과 버퍼에 문자열 함수를 바로 적용하지 않았는가

관련 노트